/*
 * 〈描述〉
 *
 * @author chenxing
 * @since 版本号
 */

package com.maorioi.failureanalyzer.component;

import com.maorioi.failureanalyzer.model.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * 〈一句话功能简述〉
 *
 * <p>〈功能详细描述〉
 *
 * @author chenxing
 * @since 版本号
 */
@Service
public class FaService {

    private static final Integer QX_NORMAL_CODE = 0;
    private static final String QX_WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=";
    private static final Integer FILE_TYPE_CLASS = 0;
    private static final Integer FILE_TYPE_OTHER = 1;
    private static final String EMOJI_START="\uD83D\uDE36\uD83D\uDE36\uD83D\uDE36\uD83D\uDE36\uD83D\uDE36";
    private static final String EMOJI_END="\uD83D\uDE36\uD83D\uDE36\uD83D\uDE36\uD83D\uDE36\uD83D\uDE36";

    @Autowired
    private RestTemplate restTemplate;
    @Autowired
    private DeveloperMobile developerMobile;
    @Autowired
    private GitProjectData gitProjectData;

    private Map<String, Set> treeCache = new HashMap<>();

    public CompileFailure notifyCompileFailure(CompileFailure compileFailure) {
        if (!gitProjectData.hasProject(compileFailure.getProjectName())) {
            throw new RuntimeException("以下项目的配置数据不存在：" + compileFailure.getProjectName());
        }
        Project project = gitProjectData.getProjectByName(compileFailure.getProjectName());
        //查找提交信息
        String msg = compileFailure.getMessage();
        if (compileFailure.getCompiler().equals(CompileFailure.COMPILER_NPM)) {
            //假设最多有一个文件报错，在"Failed to compile."一行的下面
            int index = msg.indexOf("Failed to compile.\n");
            //前端错误处理要求传入compileDir
            //假设报错文件路径为相对路径
            if (index >= 0 && compileFailure.getCompileDir() != null) {
                String[] lines = msg.substring(index).split("\n");
                Optional<String> fileLine = Stream.of(lines).filter(e -> e.matches(".*/src/.*?\\.\\w+")).findFirst();
                if (fileLine.isPresent()) {
                    String fileName = compileFailure.getCompileDir() + fileLine.get().replaceAll(".*(/src/.*?\\.\\w+)", "$1");
                    compileFailure.setCommits(findCommits(compileFailure, project, Arrays.asList(fileName)));
                }
            }
        } else if (compileFailure.getCompiler().equals(CompileFailure.COMPILER_MVN)) {
            //maven编译报错文件路径为绝对路径
            String projectName = compileFailure.getProjectName();
            String[] lines = msg.split("\n");
            //可能有多个文件报错
            Set<String> fileNameSet = Stream.of(lines).filter(e -> e.matches(".*\\.java:\\[\\d+,\\d+\\].*"))
                    .map(e -> {
                        int i = e.indexOf(projectName+"/");
                        if(i>-1){
                            e=e.substring(i+projectName.length()+1);
                        }
                        return e.replaceAll("(.*\\.java):\\[\\d+,\\d+\\].*", "$1");
                    })
                    .collect(Collectors.toSet());
            compileFailure.setCommits(findCommits(compileFailure, project, new ArrayList<>(fileNameSet)));
        } else if (compileFailure.getCompiler().equals(CompileFailure.FLYWAY)) {
            String[] lines = msg.split("\n");
            //一次只会有一个文件报错
            Optional<String> fileLine = Stream.of(lines).filter(e -> e.matches("Migration .*\\.sql failed")).findAny();
            if (fileLine.isPresent()) {
                String fileName = compileFailure.getCompileDir() + "/" + fileLine.get().replaceAll("Migration (.*\\.sql) failed", "$1");
                compileFailure.setCommits(findCommits(compileFailure, project, Arrays.asList(fileName)));
            }
        }
        //生成企信通知数据
        StringBuilder qxMsg = new StringBuilder(2000);
        qxMsg.append("编译构建出错！\n")
                .append("构建项目: ").append(compileFailure.getProjectName()).append("\n")
                .append("代码分支: ").append(compileFailure.getGitBranch()).append("\n")
                .append("构建类型: ").append(compileFailure.getCompiler()).append("\n")
                .append("构建时间: ").append(compileFailure.getBuildTime().toString()).append("\n");
        sendQxMsg(qxMsg, compileFailure, project);
        compileFailure.setMessage(null);
        return compileFailure;
    }

    public StartupFailure notifyStartupFailure(StartupFailure startupFailure) {
        String[] lines = startupFailure.getMessage().split("\n");
        Map<String, Project> classPattern = new HashMap<>();
        for (Project project : gitProjectData.getData().values()) {
            for (String p : project.getClassPattern()) {
                classPattern.put("\\s+at\\s+" + p, project);
            }
        }
        Map<String, Project> otherPattern = new HashMap<>();
        for (Project project : gitProjectData.getData().values()) {
            for (String p : project.getOtherPattern()) {
                otherPattern.put(p, project);
            }
        }
        String lastMatched = null;
        Project project = null;
        Integer fileType = -1;
        String pattern = null;
        for (int i = lines.length - 1; i >= 0; i--) {
            final String line = lines[i];
            Optional<String> otherKey = otherPattern.keySet().stream().filter(e -> line.matches(e)).findAny();
            if (otherKey.isPresent()) {
                lastMatched = line;
                project = otherPattern.get(otherKey.get());
                fileType = FILE_TYPE_OTHER;
                pattern = otherKey.get();
                break;
            }
            Optional<String> classKey = classPattern.keySet().stream().filter(e -> line.matches(e)).findAny();
            if (classKey.isPresent()) {
                lastMatched = line;
                project = classPattern.get(classKey.get());
                fileType = FILE_TYPE_CLASS;
                pattern = classKey.get();
            }
            if (lastMatched != null && line.startsWith("Caused by")) {
                break;
            }
        }
        if (lastMatched != null && project != null) {
            if (fileType == FILE_TYPE_CLASS) {
                String location = lastMatched.replaceAll(pattern, "$1");
                String[] strs = location.split("\\.");
                String path = String.join("/", Arrays.copyOfRange(strs, 0, strs.length - 2));
                String className = strs[strs.length - 2];
                Optional<String> fullPath = getGitFullPath(project, startupFailure.getGitBranch(), path);
                if (fullPath.isPresent()) {
                    String fileName = fullPath.get() + "/" + className + ".java";
                    startupFailure.setCommits(findCommits(startupFailure, project, Arrays.asList(fileName)));
                }
            } else if (fileType == FILE_TYPE_OTHER) {
                String location = lastMatched.replaceAll(pattern, "$1");
                String[] strs = location.split("/");
                String path = String.join("/", Arrays.copyOfRange(strs, 0, strs.length - 1));
                Optional<String> fullPath = getGitFullPath(project, startupFailure.getGitBranch(), path);
                if (fullPath.isPresent()) {
                    String fileName = fullPath.get() + "/" + strs[strs.length - 1];
                    startupFailure.setCommits(findCommits(startupFailure, project, Arrays.asList(fileName)));
                }
            }
        }
        StringBuilder qxMsg = new StringBuilder(2000);
        qxMsg.append("Spring Boot应用启动失败！\n")
                .append("环境名称: ").append(startupFailure.getEnvName()).append("\n")
                .append("代码分支: ").append(startupFailure.getGitBranch()).append("\n")
                .append("构建时间: ").append(startupFailure.getBuildTime().toString()).append("\n");
        sendQxMsg(qxMsg, startupFailure, project);
        startupFailure.setMessage(null);
        return startupFailure;
    }

    private void sendQxMsg(StringBuilder qxMsg, Failure failure, Project project) {
        boolean atMaintainer = false;
        List<String> mentionMobileList = new ArrayList<>();
        List<String> developerWithoutMobile = new ArrayList<>();
        if (failure.getCommits() != null && failure.getCommits().length > 0) {
            boolean atResponsible = false;
            qxMsg.append("相关文件提交记录: ").append("\n");
            for (Commit commit : failure.getCommits()) {
                Developer developer = commit.getDeveloper();
                qxMsg.append("文件: ").append(commit.getFileName()).append("\n")
                        .append("提交人: ").append(String.format("%s(%s)", developer.getUserName(), developer.getName())).append("\n")
                        .append("提交信息: ").append(commit.getMessage()).append("\n");
                if (Developer.NOT_FOUND_NAME.equals(developer.getName())) {
                    atResponsible = true;
                } else {
                    if (developer.getMobile() != null) {
                        mentionMobileList.add(developer.getMobile());
                    } else {
                        developerWithoutMobile.add(String.format("%s(%s)", developer.getUserName(), developer.getName()));
                    }
                }
            }
            qxMsg.append(EMOJI_START).append("\n错误消息如下：\n")
                    .append(cutStringLength(failure.getMessage(), 3000)).append("\n")
                    .append(EMOJI_END)
                    .append("\n");
            if (atResponsible) {
                String defaultDevMobile = developerMobile.findMobile(project.getWatcher());
                if (defaultDevMobile != null) {
                    mentionMobileList.add(defaultDevMobile);
                    qxMsg.append("存在未知提交人，请模块负责人跟进处理\n");
                } else {
                    //git-project.yml 中的default-developer 以及 developer-mobile.yml
                    qxMsg.append("存在未知提交人，请配置模块负责人\n");
                    atMaintainer = true;
                }
            }
        } else {
            qxMsg.append(EMOJI_START).append("\n错误消息如下：\n")
                    .append(cutStringLength(failure.getMessage(), 3000)).append("\n")
                    .append(EMOJI_END)
                    .append("\n");
            String defaultDevMobile = developerMobile.findMobile(project.getWatcher());
            if (defaultDevMobile != null) {
                mentionMobileList.add(defaultDevMobile);
                qxMsg.append("未找到相关文件，请模块负责人跟进处理\n");
            } else {
                qxMsg.append("未找到相关文件，需通知模块负责人处理。请配置模块负责人\n");
                atMaintainer = true;
            }
        }
        if (developerWithoutMobile.size() > 0) {
            qxMsg.append("请配置以下人员的联系信息：").append(String.join(", ", developerWithoutMobile));
            atMaintainer = true;
        }
        if (atMaintainer) {
            gitProjectData.getMaintainerMobile().ifPresent(e -> mentionMobileList.add(e));
        }
        qxNotify(qxMsg.toString(), mentionMobileList, project.getQxWebhook());
    }

    private Commit[] findCommits(Failure failure, Project project, List<String> fileNames) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.add("PRIVATE-TOKEN", project.getToken());
        List<Commit> commits = new ArrayList<>();
        for (int i = 0; i < fileNames.size(); i++) {
            String fileName = fileNames.get(i);
            List<Map> commitDatas = requestGitCommit(failure, project, fileName).getBody();
            if (commitDatas != null && commitDatas.size() > 0) {
                Map commitData = commitDatas.get(0);
                String committerName = (String) commitData.get("committer_name");
                String title = (String) commitData.get("title");
                Optional<Map> userInfoOpt = requestGitUser(project, committerName).getBody().stream()
                        .filter(e -> ((Map) e).get("name").equals(committerName) || ((Map) e).get("username").equals(committerName))
                        .findAny();
                Developer developer;
                if (userInfoOpt.isPresent()) {
                    Map userInfo = userInfoOpt.get();
                    developer = new Developer();
                    developer.setName((String) userInfo.get("name"));
                    developer.setUserName((String) userInfo.get("username"));
                    developer.setMobile(developerMobile.findMobile(developer.getUserName()));
                } else {
                    developer = new Developer();
                    developer.setUserName(committerName);
                    developer.setName(Developer.NOT_FOUND_NAME);
                }
                commits.add(new Commit(developer, fileName, title));
            }
        }
        Commit[] ret = new Commit[commits.size()];
        for (int i = 0; i < commits.size(); i++) {
            ret[i] = commits.get(i);
        }
        return ret;
    }

    private String cutStringLength(String str, int length) {
        if (str.length() > length) {
            return "(省略前面的字符)" + str.substring(str.length() - length);
        }
        return str;
    }

    private ResponseEntity<List> requestGitCommit(Failure failure, Project project, String fileName) {
        String url = gitProjectData.getUrl() + String.format("/api/v4/projects/%s/repository/commits?ref_name=%s&until=%s&path=%s",
                project.getId(), failure.getGitBranch(), failure.getBuildTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME) + "Z", fileName);
        RequestEntity<Void> requestEntity = RequestEntity.get(url).header("PRIVATE-TOKEN", project.getToken()).build();
        return restTemplate.exchange(requestEntity, List.class);
    }

    private ResponseEntity<List> requestGitUser(Project project, String username) {
        String url = gitProjectData.getUrl() + "/api/v4/search?scope=users&search=" + username;
        RequestEntity<Void> requestEntity = RequestEntity.get(url).header("PRIVATE-TOKEN", project.getToken()).build();
        return restTemplate.exchange(requestEntity, List.class);
    }

    private Optional<String> getGitFullPath(Project project, String branch, String suffix) {
        Set<String> paths = this.treeCache.get(project.getId() + "#" + branch);
        if (paths == null || !paths.stream().filter(e -> e.endsWith(suffix)).findAny().isPresent()) {
            paths = new HashSet<>();
            String url = gitProjectData.getUrl() + String.format("/api/v4/projects/%s/repository/tree?ref=%s&recursive=true&per_page=100",
                    project.getId(), branch);
            RequestEntity<Void> requestEntity = RequestEntity.get(url)
                    .header("PRIVATE-TOKEN", project.getToken())
                    .build();
            ResponseEntity<List> responseEntity = restTemplate.exchange(requestEntity, List.class);
            paths.addAll((Collection<? extends String>) responseEntity.getBody()
                    .stream().map(e -> ((Map) e).get("path").toString()).collect(Collectors.toList()));
            HttpHeaders httpHeaders = responseEntity.getHeaders();
            int totalPage = Integer.valueOf(httpHeaders.get("X-Total-Pages").get(0));
            int currentPage = Integer.valueOf(httpHeaders.get("X-Page").get(0));
            while (currentPage < totalPage) {
                requestEntity = RequestEntity.get(url + "&page=" + (++currentPage))
                        .header("PRIVATE-TOKEN", project.getToken())
                        .build();
                responseEntity = restTemplate.exchange(requestEntity, List.class);
                paths.addAll((Collection<? extends String>) responseEntity.getBody()
                        .stream().map(e -> ((Map) e).get("path").toString()).collect(Collectors.toList()));
            }
            this.treeCache.put(project.getId() + "#" + branch, paths);
        }
        return paths.stream().filter(e -> e.endsWith(suffix)).findAny();
    }

    public void qxNotify(String content, List<String> mentionMobileList, String key) {
        HttpHeaders httpHeaders = new HttpHeaders();
        httpHeaders.setContentType(MediaType.APPLICATION_JSON);
        Map msg = new HashMap();
        Map data = new HashMap();
        msg.put("msgtype", "text");
        msg.put("text", data);
        data.put("content", content);
        data.put("mentioned_mobile_list", mentionMobileList);
        ResponseEntity<Map> responseEntity = restTemplate.exchange(QX_WEBHOOK_URL + key,
                HttpMethod.POST, new HttpEntity<>(msg, httpHeaders), Map.class);
        validateQxResponse(responseEntity.getBody());
    }

    private void validateQxResponse(Map responseData) {
        if (!QX_NORMAL_CODE.equals(responseData.get("errcode"))) {
            throw new RuntimeException("企业微信通知失败：" + responseData.get("errmsg"));
        }
    }

    public List listLatestCommit(String project, String branch, String filePath) {
        Project pro = gitProjectData.getProjectByName(project);
        String url = gitProjectData.getUrl() + String.format("/api/v4/projects/%s/repository/commits?ref_name=%s&path=%s",
                pro.getId(), branch, filePath);
        RequestEntity<Void> requestEntity = RequestEntity.get(url).header("PRIVATE-TOKEN", pro.getToken()).build();
        return restTemplate.exchange(requestEntity, List.class).getBody();
    }

    public void alterProjectWatcher(Map<String, String> watchers) {
        for (String projectName : watchers.keySet()) {
            Project project = gitProjectData.getProjectByName(projectName);
            if (project != null) {
                project.setWatcher(watchers.get(projectName));
            }
        }
    }
}
